package com.skyline.energy.parser;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.skyline.energy.utils.CommonUtils;

public final class ExpressionParser {
	private static final int DEFAULT_CACHE_LIMIT = 512;
	private static final char[] PARAMETER_SEPARATORS = new char[] { '"', '\'', ':', '&', ',', ';', '(', ')', '|', '=',
			'+', '-', '*', '%', '/', '\\', '<', '>', '^' };

	private static final String[] START_SKIP = new String[] { "'", "\"", "--", "/*" };

	private static final String[] STOP_SKIP = new String[] { "'", "\"", "\n", "*/" };

	private static final Map<String, ParsedExpression> PARSEDE_XPRESSION_CACHE = new HashMap<String, ParsedExpression>(
			DEFAULT_CACHE_LIMIT);

	private ExpressionParser() {
	}

	public static synchronized ParsedExpression parseExpression(String expression) {
		ParsedExpression parsedExpression = PARSEDE_XPRESSION_CACHE.get(expression);
		if (parsedExpression == null) {
			parsedExpression = parse(expression);
			PARSEDE_XPRESSION_CACHE.put(expression, parsedExpression);
		}
		return parsedExpression;
	}

	private static ParsedExpression parse(String expression) {
		CommonUtils.assertNotNull(expression, "Expression(SQL) must not be null");

		Set<String> namedParameters = new HashSet<String>();
		ParsedExpression parsedExpression = new ParsedExpression(expression);

		char[] statement = expression.toCharArray();
		int namedParameterCount = 0;
		int unnamedParameterCount = 0;
		int totalParameterCount = 0;

		int i = 0;
		while (i < statement.length) {
			int skipToPosition = skipCommentsAndQuotes(statement, i);
			if (i != skipToPosition) {
				if (skipToPosition >= statement.length) {
					break;
				}
				i = skipToPosition;
			}
			char c = statement[i];
			if (c == ':' || c == '&') {
				int j = i + 1;
				if (j < statement.length && statement[j] == ':' && c == ':') {
					// Postgres-style "::" casting operator - to be skipped.
					i = i + 2;
					continue;
				}
				j = endIndexOfParameter(statement, i, j);
				if (j - i > 1) {
					String parameter = expression.substring(i + 1, j);
					namedParameterCount = addNamedParameter(namedParameters, parameter, namedParameterCount);
					parsedExpression.addNamedParameter(parameter, i, j);
					totalParameterCount++;
				}
				i = j - 1;
			} else if (c == '?') {
				unnamedParameterCount++;
				totalParameterCount++;
			}
			i++;
		}
		parsedExpression.setNamedParameterCount(namedParameterCount);
		parsedExpression.setUnnamedParameterCount(unnamedParameterCount);
		parsedExpression.setTotalParameterCount(totalParameterCount);
		return parsedExpression;
	}

	private static int addNamedParameter(Set<String> namedParameters, String parameter, int namedParameterCount) {
		if (!namedParameters.contains(parameter)) {
			namedParameters.add(parameter);
			namedParameterCount++;
		}
		return namedParameterCount;
	}

	private static int endIndexOfParameter(char[] statement, int i, int j) {
		while (j < statement.length && !isParameterSeparator(statement[j])) {
			j++;
		}
		return j;
	}

	private static int skipCommentsAndQuotes(char[] statement, int position) {
		for (int i = 0; i < START_SKIP.length; i++) {
			if (statement[position] == START_SKIP[i].charAt(0)) {
				boolean match = true;
				for (int j = 1; j < START_SKIP[i].length(); j++) {
					if (!(statement[position + j] == START_SKIP[i].charAt(j))) {
						match = false;
						break;
					}
				}
				if (match) {
					return endIndexOfCommentsAndQuotes(statement, i, position);
				}

			}
		}
		return position;
	}

	private static int endIndexOfCommentsAndQuotes(char[] statement, int i, int position) {
		int offset = START_SKIP[i].length();
		for (int m = position + offset; m < statement.length; m++) {
			if (statement[m] == STOP_SKIP[i].charAt(0)) {
				boolean endMatch = true;
				int endPos = m;
				for (int n = 1; n < STOP_SKIP[i].length(); n++) {
					if (m + n >= statement.length) {
						// last comment not closed properly
						return statement.length;
					}
					if (!(statement[m + n] == STOP_SKIP[i].charAt(n))) {
						endMatch = false;
						break;
					}
					endPos = m + n;
				}
				if (endMatch) {
					// found character sequence ending comment or quote
					return endPos + 1;
				}
			}
		}
		// character sequence ending comment or quote not found
		return statement.length;
	}

	private static boolean isParameterSeparator(char c) {
		if (Character.isWhitespace(c)) {
			return true;
		}
		for (char separator : PARAMETER_SEPARATORS) {
			if (c == separator) {
				return true;
			}
		}
		return false;
	}

}
